package com.yumeng.framework.auth.shiro.filter;

import com.yumeng.common.api.ApiCode;
import com.yumeng.common.utils.IpUtils;
import com.yumeng.common.utils.ResponseUtils;
import com.yumeng.framework.auth.bean.VerifyLoginInfo;
import com.yumeng.framework.auth.exception.ShiroAuthException;
import com.yumeng.framework.auth.exception.TokenAuthException;
import com.yumeng.framework.auth.shiro.ShiroExceptionUtils;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import java.util.Arrays;

/**
 * 无状态模式过滤器
 * @author wxd
 * @date 2022/6/1 13:47
 */
@Slf4j
public abstract class AbstractStatelessFilter extends AccessControlFilter {

    public static final String PERMISSIVE = "permissive";

    public abstract String getFilteredFlagName();


    /**
     * 验证是否可以访问
     * 判断token有效性、认证成功后判断更新
     * @param request
     * @param response
     * @param mappedValue
     * @return
     * @throws Exception
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        HttpServletRequest httpRequest = WebUtils.toHttp(request);
        log.debug("Stateless过滤器-isAccessAllowed:{}", httpRequest.getRequestURI());
        //登录接口直接放行 言下之意，不管是登录页面，还是登录post接口或者getToken接口，都直接放行，不做处理。相当于anon
        //AuthenticatingFilter中，登录接口是需要进入onAccessDenied中处理的，要在Filter中实现自动登录
        if(isLoginRequest(request, response)){
            log.info("Stateless过滤器-登录url直接放行:{}", httpRequest.getRequestURI());
            return true;
        }
        Boolean afterFiltered = (Boolean)(request.getAttribute(getFilteredFlagName()));
        if(BooleanUtils.isTrue(afterFiltered)){//其实不需要这个,添加此判断可用于辅助排除一些未处理的访问，如/error
            String msg = String.format("Stateless过滤器-已过滤标志:%s", httpRequest.getRequestURI());
            log.error(msg);
            throw new ShiroAuthException(msg);
        }
        boolean allowed = false;
        try {
            AuthenticationToken token = createToken(request, response);
            if (token != null){
                allowed = executeLogin(token, request, response);
            }
        }catch (AuthenticationException | AuthorizationException e ){
            //抛出后在GlobalExceptionHandler中处理,后续可继续添加
            String msg = null;
            if (e.getCause() instanceof TokenAuthException){
                msg = e.getCause().getMessage();
            }else {
                msg = ShiroExceptionUtils.exceptionMessageMap.get(e.getClass());
            }
            msg = ObjectUtils.getIfNull(msg, e.getMessage());
            throw new ShiroAuthException(msg, e);
        }catch (Exception e){
            allowed = false;
            log.error("令牌认证失败", e);
        }
        return allowed || isPermissive(mappedValue);
    }

    /**
     * 拒绝访问处理业务
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @SneakyThrows
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response){
        String alreadyFilteredAttributeName = getAlreadyAccessDeniedFilteredAttributeName();
        if (request.getAttribute(alreadyFilteredAttributeName) == null){
            ResponseUtils.writeJson((HttpServletResponse) response, ApiCode.FORBIDDEN, "访问被拒绝");
        }
        return false;
    }

    /**
     * 执行登录
     * 无状态模式，每次访问都需登录一次，以获取当前会话的身份信息
     * 可通过缓存身份信息，来降低每次登录操作的消耗
     * @param token
     * @param request
     * @param response
     * @return
     */
    protected boolean executeLogin(AuthenticationToken token, ServletRequest request, ServletResponse response){
        try{
            VerifyLoginInfo info = onPreLogin(token, request, response);
            if (info != null && !info.isSucc()){
                throw new AuthenticationException("登录预处理失败");
            }
            Subject subject = getSubject(request, response);
            subject.login(token);
            return onLoginSuccess(token, subject, info, request, response);
        } catch (AuthenticationException e){
            return onLoginFailure(token, e, request, response);
        }
    }

    /**
     * 创建token
     * 此处不判断有效性，只判断有无，有效性放在登录前置操作中统一处理
     * @param request
     * @param response
     * @return
     */
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response){
        AuthenticationToken token = doCreateToken(request, response);
        if (token == null){
            throw new AuthenticationException("Token对象创建失败，请重新登录后再试");
        }
        return token;
    }

    protected abstract AuthenticationToken doCreateToken(ServletRequest request, ServletResponse response);


    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())){
            httpServletResponse.setStatus(HttpStatus.NO_CONTENT.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    @Override
    protected void postHandle(ServletRequest request, ServletResponse response) throws Exception {
        request.setAttribute(getFilteredFlagName(), true);
    }

    /**
     * 登录前置处理
     * @param token
     * @param request
     * @param response
     * @return
     */
    protected VerifyLoginInfo onPreLogin(AuthenticationToken token, ServletRequest request, ServletResponse response){
        return null;
    }

    /**
     * 登录成功后
     * @param token
     * @param subject
     * @param request
     * @param response
     * @return
     */
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, VerifyLoginInfo info, ServletRequest request, ServletResponse response){
        return true;
    }

    /**
     * 登录失败后
     * @param token
     * @param e
     * @param request
     * @param response
     * @return
     */
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        log.error("Validate Token fail, token:{}, error:{}", token.toString(), e.getMessage());
        return false;
    }

    /**
     * 获取ip地址
     * @param request
     * @return
     */
    protected String getHost(ServletRequest request) {
        return IpUtils.getRequestIp(WebUtils.toHttp(request));
    }

    /**
     * 判断是否可以直接放行
     * @param mappedValue
     * @return
     */
    protected boolean isPermissive(Object mappedValue) {
        if(mappedValue != null) {
            String[] values = (String[]) mappedValue;
            return Arrays.binarySearch(values, PERMISSIVE) >= 0;
        }
        return false;
    }

    /**
     * 返回请求属性的名称，用于筛选已经对“拒绝访问”进行处理的请求
     * 如“登录成功”，虽然拒绝后续访问，但是已经向前端返回登录成功信息，因此不需要进一步处理
     * @return
     */
    protected String getAlreadyAccessDeniedFilteredAttributeName() {
        String name = getName();
        if (name == null) {
            name = getClass().getName();
        }
        return name + ".ALREADY_ACCESS_DENIED";
    }

}
